Skip to content

ref 这一种访问 DOM 的主要方式。然而,useRef() 比 ref 属性更有用。它可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式.

当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

场景 1:只在更新时运行 useEffect

使用一个可变的 ref 手动存储一个布尔值来表示是首次渲染还是后续渲染.

jsx
function Example() {
  const [count, setCount] = useState(0);

  const prevCountRef = useRef(false);
  useEffect(() => {
    if (prevCountRef.current) {
      console.log("只在更新时候执行");
    } else {
      console.log("首次渲染执行");
      prevCountRef.current = true;
    }
  });

  return (
    <div>
      <div>{count}</div>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        +
      </button>
    </div>
  );
}

抽成自定义 hook:

jsx
function Example() {
  const [count, setCount] = useState(0);

  const update = useUpdate();
  console.log(update, "是否更新");

  return (
    <div>
      <div>{count}</div>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        +
      </button>
    </div>
  );
}

function useUpdate() {
  const ref = useRef(false);
  useEffect(() => {
    ref.current = true;
  });
  return ref.current;
}

场景 2:获取上一轮的 props 或 state

为什么 ref.current 拿到是上次的值?原因:

  1. useEffect 很重要的一点是:它是在每次渲染之后才会触发的,是延迟执行的。
  2. return 语句是同步的,所以 return 的时候,ref.current 还是旧值。
  3. 以下代码的执行顺序是 1 3 2
jsx
function App() {
  const [count, setCount] = useState(0);

  const prevCountRef = useRef();

  console.log("1", count);
  useEffect(() => {
    console.log("2.", count);
    prevCountRef.current = count;
  });

  const prevCount = prevCountRef.current;

  console.log(`3.之前的状态: ${prevCount};现在状态: ${count}`);
  return (
    <div>
      <div>{count}</div>
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        +
      </button>
    </div>
  );
}

抽取成自定义 Hook:

jsx
export default function usePrevious(value) {
  const ref = useRef();

  useEffect(() => {
    ref.current = value;
  }, [value]);

  return ref.current;
}

场景 3:解决 hooks 时,由于异步闭包无法获取最新 state 的问题

jsx
import React, { useState, useEffect, useRef } from "react";

const RefComponent = () => {
  // 使用 useState 存放和改变展示的 number
  const [number, setNumber] = useState(0);
  // 使用 useRef 生成一个独立的 ref 对象
  // 在它的 current 属性单独存放一个展示的 number, 初始值为 0
  const numRef = useRef(0);

  function incrementAndDelayLogging() {
    // 点击按钮 number + 1
    setNumber(number + 1);
    // 同时 ref 对象的 current 属性值也 + 1
    numRef.current++;
    // 定时器函数中产生了闭包, 这里 number 的是组件更新前的 number 对象, 所以值一直会滞后 1
    setTimeout(() => alert(`state: ${number} | ref: ${numRef.current}`), 1000);
  }

  // 直接渲染的组件是正常情况, 可以获取到最新的 state,
  // 所以 ref.current 和 state 存储的值显示一致
  return (
    <div>
      <h1>solving closure by useRef</h1>
      <button onClick={incrementAndDelayLogging}>alert in setTimeout</button>
      <h4>state: {number}</h4>
      <h4>ref: {numRef.current}</h4>
    </div>
  );
};

场景 4:记录 DOM 尺寸并避免重复测量

jsx
function useDomRect<T extends HTMLElement>() {
  const ref = useRef<T | null>(null);
  const rectRef = useRef<DOMRect>();

  useLayoutEffect(() => {
    if (ref.current) {
      rectRef.current = ref.current.getBoundingClientRect();
    }
  });

  return { ref, rect: rectRef.current };
}

useLayoutEffect 保证在浏览器绘制前拿到尺寸,而 rectRef 缓存住上一次的 DOMRect,方便 diff。

场景 5:暴露命令式句柄

ts
export interface PhotoHandle {
  focus: () => void;
}

const Photo = forwardRef<PhotoHandle>((props, ref) => {
  const inputRef = useRef<HTMLInputElement>(null);
  useImperativeHandle(ref, () => ({
    focus() {
      inputRef.current?.focus();
    },
  }));
  return <input ref={inputRef} />;
});

Ref 不只有 DOM,它还可以承载自定义的命令式 API,通过 useImperativeHandle 暴露给父组件。

纠错与补充

  • useRef 持有的对象在整个生命周期内都不会变化,因此非常适合保存“不会触发渲染的状态”;如果你发现某段逻辑需要既读又更新 UI,就应该改回 useState
  • 在严格模式下 React 会执行组件初始化两次,但 useRef 始终返回同一个引用,因此不会重复注册事件;但你仍然需要在副作用里正确清理监听器,防止 ref.current 指向已经卸载的节点。
  • useRef(null) 并不会立刻赋值,需要到 render 之后 React 才会把 DOM 节点挂到 current 上,所以在事件回调里使用前务必判空。

Copyright ©2025 moweiwei